//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: StorageObjectReader_1.cs
//
//  Description: Forward-only cursor for objects from a storage data source.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using Chili.Opf3.Mapping;
using System.Xml.Serialization;

namespace Chili.Opf3.Storages
{
	/// <summary>
	/// Provides a way of reading a forward-only stream of objects from a storage data source.
	/// </summary>
	internal sealed class StorageObjectReader<T> : ObjectReader<T>
	{
		private IDataReader _dataReader;
		private ObjectDataAdapter _objectDataAdapter;
		private T _current;
		private bool _hasObjects;
		private bool _finishedReading;
		private ObjectPool _pool;
		private ObjectReaderSchema _schema;

		/// <summary>
		/// Creates a new instance of the <see cref="StorageObjectReader{T}">StorageObjectReader Class</see>.
		/// </summary>
		/// <param name="context">The <see cref="ObjectContext">ObjectContext</see> associated with the 
		/// ObjectReader. It is required to get the <see cref="ObjectInfo">ObjectInfo</see> object
		/// of created objects.</param>
		/// <param name="dataReader">IDataReader that gets the data from the physical storage.</param>
		/// <exception cref="System.ArgumentNullException">DataReader is null.</exception>
		internal StorageObjectReader(ObjectContext context, IDataReader dataReader)
			: base(context)
		{
			TypeMapping typeMapping = TypeMapping.GetTypeMapping(typeof(T));
			// Check if the type has the right to be loaded.
			if ((typeMapping.PersistentAttribute.Rights & PersistentRights.Load) == 0)
				throw new InvalidOperationException("The persistent object type has no right to be loaded.");
			
			// DataReader can't be null.
			if (dataReader == null)
				throw new ArgumentNullException("dataReader");

			_dataReader = dataReader;
			_objectDataAdapter = new ObjectDataAdapter(context.Storage);
			// Get the ObjectPool that contains empty objects.
			_pool = ObjectPool.Current;

			// Build the ObjectReaderSchema.
			_schema = new ObjectReaderSchema(_dataReader, typeof(T));
		}

		/// <summary>
		/// Advances the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> to one of the next records. 
		/// </summary>
		/// <param name="step">The step that is performed while reading.</param>
		/// <returns>Returns true if the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> could be
		/// advanced to the next object.</returns>
		protected override bool InternalRead(int step)
		{
			// Get the persistent type.
			Type type = TypeMapping.GetTypeMapping(typeof(T)).PersistentType;
			while (true)
			{
				// Advance the internal IDataReader.
				if (!AdvanceInternalDataReader(step))
					return false;
				// The reader has at least one object.
				_hasObjects = true;

				// Create the EventArgs class an invoke the event.
				FetchingStorageRecordEventArgs eArgs = new FetchingStorageRecordEventArgs(_dataReader, type);
				OnFetchingStorageRecord(eArgs);
				if (eArgs.Ignore)
					continue;

				if (type != eArgs.Type)
				{
					// Check if the returned type is valid.
					if (!eArgs.Type.IsSubclassOf(type))
						throw new InvalidCastException("Only subtypes of the current type are allowed when selecting new type.");

					// Set the returned type as the type of the object to populate.
					type = eArgs.Type;
					// Recreate the schema here. Required since the type might be changed.
					_schema = new ObjectReaderSchema(_dataReader, type);
				}

				// Check if the type implements the given interface.
				if (typeof(IPersistentTypeSelector).IsAssignableFrom(type))
				{
					// Create an instance of the class.
					IPersistentTypeSelector selector = (IPersistentTypeSelector)ObjectPool.Current.CreateObject(type, null);
					// Fetch the actual type.
					Type newType = selector.FetchPersistentType(_dataReader);
					// Skip the current record if the returned type is null.
					if (newType == null)
						continue;

					if (newType != type)
					{
						// Check if the returned type is valid.
						if (!newType.IsSubclassOf(type))
							throw new InvalidCastException("Only subtypes of the current type are allowed when selecting new type.");

						type = newType;
						// Recreate the schema here. Required since the type might be changed.
						_schema = new ObjectReaderSchema(_dataReader, type);
					}
				}

				break;
			}

			// Create a new object and set that as current.
			PopulateObject(type);
			return true;
		}

		/// <summary>
		/// Reads and populates a new object and sets that as current.
		/// </summary>
		/// <param name="type">The type of the new persistent object.</param>
		private void PopulateObject(Type type)
		{
			// Check we have to deal with an anonymous type here.
			if (TypeMapping.GetTypeMapping(type).IsAnonymousType)
			{
				_current = (T)_objectDataAdapter.FillObject(type, (IDataRecord)_dataReader, _schema);
				return;
			}
			
			// Get an object of the given type from the pool.
			T obj = (T)_pool.GetObject(type);
			ObjectInfo objectInfo = this.Context.GetObjectInfo(obj);
			// Invoke the loading event.
			OnLoading(new ObjectLoadingEventArgs(obj, objectInfo));

			ObjectNotificationEventArgs e = null;
			if (obj is IObjectNotification)
			{
				e = new ObjectNotificationEventArgs(objectInfo, this.Context, PersistentOperations.Load, PersistDepths.None);
				((IObjectNotification)obj).OnBeforePopulated(e);
			}

			// Fill the object with data.
			_objectDataAdapter.FillObject(obj, (IDataRecord)_dataReader, objectInfo, _schema);

			// this object is the one that might get fetched from the identity map.
			T identityObject = default(T);

			// check if we should use the identity map to understand whether this object is already in memory.
			if (this.Context.IdentityMap.Enabled && this.Context.IdentityMap.TryGetValue(obj, out identityObject))
			{
				obj = identityObject;
			}
			else
			{
				// if we use an identity map but the object hasn't been found it needs to be added to the
				// identity map.
				if (this.Context.IdentityMap.Enabled)
					this.Context.IdentityMap.Add(obj);

				// Connect the related objects.
				this.Context.RelationsManager.ConnectRelated(obj);
				// Signalize that the object has been loaded.
				objectInfo.ObjectState = ObjectStates.Loaded;

				if (obj is IObjectNotification)
					((IObjectNotification)obj).OnAfterPopulated(e);

				// Notifies the ConcurrencyManager.
				NotifyConcurrencyManager(obj);
			}

			// Add the object to the cache if a cache is available.
			if (this.Context.CacheManager != null)
				this.Context.CacheManager.AddPersistentObject(obj);
			// Set it as current.
			_current = obj;
		}

		/// <summary>
		/// Advances the internal IDataReader.
		/// </summary>
		/// <param name="step">The amount of rows the IDataReader is advanced.</param>
		private bool AdvanceInternalDataReader(int step)
		{
			if (_finishedReading)
				return false;

			for (int i = 0; i < step; i++)
			{
				// Read the next item.
				if (!_dataReader.Read())
				{
					_finishedReading = true;
					return false;
				}
			}
			return true;
		}

		/// <summary>
		/// Closes the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> object.
		/// </summary>
		public override void Close()
		{
			if (_dataReader != null && !_dataReader.IsClosed)
				_dataReader.Close();
			else
			{
				// Notify the user that the ObjectReader has been closed.
				Tracing.Current.TraceVerbose("ObjectReader has been already closed.");
			}
		}

		/// <summary>
		/// Indicates whether the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> has been closed. 
		/// </summary>
		/// <value>Returns true if the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> is closed.</value>
		[XmlIgnore]
		public override bool IsClosed
		{
			get { return (_dataReader == null || _dataReader.IsClosed); }
		}

		/// <summary>
		/// Returns whether the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> contains 
		/// one or more rows. 
		/// </summary>
		/// <value>Returns true if the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> has fetched one
		/// ore more objects.</value>
		[XmlIgnore]
		public override bool HasObjects
		{
			get
			{
				if (IsClosed)
					return false;
				return _hasObjects;
			}
		}

		/// <summary>
		/// Returns if the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> has finished reading. 
		/// Is true after Read returned false.
		/// </summary>
		/// <value>Returns true if the <see cref="Opf3.ObjectReader{T}">ObjectReader</see> has finished reading.</value>
		[XmlIgnore]
		public override bool FinishedReading
		{
			get { return _finishedReading; }
		}

		/// <summary>
		/// Returns the current object managed by the ObjectReader.
		/// </summary>
		/// <returns></returns>
		protected override T GetCurrentObject()
		{
			return _current;
		}

		#region IDisposable Members

		/// <summary>
		/// Disposes the <see cref="Opf3.ObjectReader{T}">ObjectReader</see>.
		/// </summary>
		public override void Dispose()
		{
			base.Dispose();

			if (_dataReader != null)
			{
				_dataReader.Dispose();
				_dataReader = null;
			}
		}

		#endregion
	}
}
